all repos — weimar @ 74fc3e1e24b47e370bcf5ea6b4e016cb50630920

Unnamed repository; edit this file 'description' to name the repository.

web/src/routes/image/[id]/+page.svelte (view raw)

  1<script lang="ts">
  2	import { onMount } from 'svelte';
  3	import { getImage, imageDownloadUrl, thumbnailUrl, type ImageDetail } from '$lib/api';
  4
  5	let { params } = $props();
  6	let id = $derived(parseInt(params.id));
  7
  8	let img = $state<ImageDetail | null>(null);
  9	let loading = $state(true);
 10	let error = $state('');
 11
 12	onMount(async () => {
 13		try {
 14			img = await getImage(id);
 15		} catch (err) {
 16			error = err instanceof Error ? err.message : 'Failed to load image';
 17		} finally {
 18			loading = false;
 19		}
 20	});
 21
 22	function formatSize(bytes: number): string {
 23		if (bytes < 1024) return bytes + ' B';
 24		if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
 25		return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
 26	}
 27
 28	function formatDate(s: string): string {
 29		return new Date(s).toLocaleString();
 30	}
 31</script>
 32
 33{#if loading}
 34	<p class="state-msg">Loading...</p>
 35{:else if error}
 36	<p class="state-msg error">{error}</p>
 37{:else if img}
 38	<div class="page-head">
 39		<h1>{img.filename}</h1>
 40		<div class="head-line"></div>
 41	</div>
 42
 43	<div class="layout">
 44		<div class="preview">
 45			{#if img.thumbnail_path}
 46				<img src={imageDownloadUrl(img.id)} alt={img.filename} />
 47			{:else}
 48				<div class="no-thumb">NO THUMBNAIL</div>
 49			{/if}
 50		</div>
 51
 52		<div class="meta">
 53			<div class="meta-block">
 54				<span class="meta-label">ID</span>
 55				<span class="meta-value">#{img.id}</span>
 56			</div>
 57			<div class="meta-block">
 58				<span class="meta-label">DIMENSIONS</span>
 59				<span class="meta-value">{img.width} &times; {img.height}</span>
 60			</div>
 61			<div class="meta-block">
 62				<span class="meta-label">FILE SIZE</span>
 63				<span class="meta-value">{formatSize(img.file_size)}</span>
 64			</div>
 65			<div class="meta-block">
 66				<span class="meta-label">TYPE</span>
 67				<span class="meta-value">{img.mime_type}</span>
 68			</div>
 69			<div class="meta-block">
 70				<span class="meta-label">UPLOADED BY</span>
 71				<span class="meta-value">{img.uploader_username}</span>
 72			</div>
 73			<div class="meta-block">
 74				<span class="meta-label">UPLOADED</span>
 75				<span class="meta-value">{formatDate(img.created_at)}</span>
 76			</div>
 77			<div class="meta-block tags-block">
 78				<span class="meta-label">TAGS</span>
 79				<div class="tag-pills">
 80					{#if img.tags_detail.length > 0}
 81						{#each img.tags_detail as t (t.tag)}
 82							<a href="/browse?tag={encodeURIComponent(t.tag)}" class="tag-pill" title="Used {t.usage_count} time{t.usage_count === 1 ? '' : 's'}">
 83								<span class="tag-name">{t.tag}</span>
 84								<span class="tag-count">{t.usage_count}</span>
 85							</a>
 86						{/each}
 87					{:else}
 88						<span class="none">none</span>
 89					{/if}
 90				</div>
 91			</div>
 92
 93			<a href={imageDownloadUrl(img.id)} class="btn" download={img.filename}>
 94				DOWNLOAD ORIGINAL
 95			</a>
 96		</div>
 97	</div>
 98
 99	<p class="back"><a href="/browse">&larr; BACK TO BROWSE</a></p>
100{/if}
101
102<style>
103	.state-msg {
104		padding: 2rem 0;
105		font-size: 1.1rem;
106		color: #888;
107	}
108	.error {
109		color: #c62828;
110	}
111
112	.page-head {
113		margin-bottom: 1.5rem;
114	}
115	.page-head h1 {
116		word-break: break-all;
117	}
118	.head-line {
119		height: 3px;
120		background: #c62828;
121		width: 3rem;
122		margin-top: 0.4rem;
123	}
124
125	.layout {
126		display: flex;
127		gap: 2rem;
128		flex-wrap: wrap;
129	}
130	.preview {
131		flex: 1;
132		min-width: 300px;
133		background: #1a1a1a;
134		display: flex;
135		align-items: center;
136		justify-content: center;
137	}
138	.preview img {
139		display: block;
140		width: 100%;
141		height: auto;
142		max-height: 70vh;
143		object-fit: contain;
144	}
145	.no-thumb {
146		padding: 4rem 2rem;
147		color: #555;
148		font-family: 'Oswald', sans-serif;
149		font-weight: 500;
150		font-size: 0.9rem;
151		letter-spacing: 0.12em;
152	}
153
154	.meta {
155		width: 260px;
156		flex-shrink: 0;
157		display: flex;
158		flex-direction: column;
159		gap: 1rem;
160	}
161	.meta-block {
162		display: flex;
163		flex-direction: column;
164		gap: 0.15rem;
165	}
166	.meta-label {
167		font-family: 'Oswald', sans-serif;
168		font-weight: 500;
169		font-size: 0.7rem;
170		letter-spacing: 0.15em;
171		color: #999;
172	}
173	.meta-value {
174		font-family: 'Inter', sans-serif;
175		font-size: 0.9rem;
176		font-weight: 600;
177		color: #1a1a1a;
178	}
179
180	.tags-block {
181		flex: 1;
182	}
183	.tag-pills {
184		display: flex;
185		flex-wrap: wrap;
186		gap: 0.35rem;
187	}
188	.tag-pill {
189		display: inline-flex;
190		align-items: center;
191		gap: 0.3rem;
192		background: #1a1a1a;
193		padding: 0.25rem 0.5rem 0.25rem 0.65rem;
194		font-family: 'Inter', sans-serif;
195		font-size: 0.78rem;
196		font-weight: 500;
197		color: #eee;
198		text-decoration: none;
199		transition: background 0.15s;
200	}
201	.tag-pill:hover {
202		background: #c62828;
203		text-decoration: none;
204	}
205	.tag-name {
206		font-weight: 600;
207	}
208	.tag-count {
209		background: rgba(255, 255, 255, 0.12);
210		padding: 0.05rem 0.35rem;
211		font-size: 0.68rem;
212		font-weight: 600;
213		color: rgba(255, 255, 255, 0.65);
214		min-width: 1.1rem;
215		text-align: center;
216	}
217	.tag-pill:hover .tag-count {
218		background: rgba(0, 0, 0, 0.2);
219	}
220	.none {
221		color: #bbb;
222		font-size: 0.85rem;
223	}
224
225	.btn {
226		display: inline-flex;
227		align-items: center;
228		align-self: flex-start;
229		padding: 0.55rem 1.25rem;
230		background: #c62828;
231		color: #fff;
232		border: none;
233		font-family: 'Oswald', sans-serif;
234		font-weight: 500;
235		font-size: 0.8rem;
236		letter-spacing: 0.1em;
237		text-decoration: none;
238		text-transform: uppercase;
239		cursor: pointer;
240		transition: background 0.15s;
241		margin-top: 0.5rem;
242	}
243	.btn:hover {
244		background: #b71c1c;
245		text-decoration: none;
246	}
247
248	.back {
249		margin-top: 2rem;
250	}
251	.back a {
252		font-family: 'Oswald', sans-serif;
253		font-weight: 500;
254		font-size: 0.85rem;
255		letter-spacing: 0.1em;
256	}
257</style>